import matplotlib
import numpy as np
import time
import matplotlib.pyplot as plt


class Point:
    def __init__(self, t):
        self.x = t[0]
        self.y = t[1]
    def __repr__(self):
        return "p({:.2f}, {})".format(self.x, self.y)

def count_time(func):
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        func(*args, **kwargs)
        end = time.perf_counter()
        print(f"time usage: {end - start:.4f}")
    return wrapper

class Curve:
    __origin_point_list: list[Point]
    point_list: list[Point]

    @property
    def origin_point_list(self):
        return self.__origin_point_list

    @staticmethod
    def divide(p1, p2, dn=4):
        if dn == 2:
            # 将曲线分为2段
            return [
                Point((
                    p1.x + (p2.x - p1.x) / dn,
                    p1.y + (p2.y - p1.y) / dn,
                ))
            ]

        return [
            Point((
                p1.x + (p2.x - p1.x) / dn * i,
                p1.y + (p2.y - p1.y) / dn * i,
            ))
            for i in [1, dn - 1]
        ]

    def __init__(self, point_list:list[Point]):
        self.__origin_point_list = point_list
        self.point_list = point_list
        self.__plt_history = []

    def sub(self):
        point_list_end_index = len(self.point_list) - 2
        nps = []
        for i, p in enumerate(self.point_list):
            if i == 0:
                nps.append(p)
                nps += self.divide(p, self.point_list[i + 1], 2)
            elif i == point_list_end_index:
                nps += self.divide(p, self.point_list[i + 1], 2)
                nps.append(self.point_list[i + 1])
                break
            else:
                nps += self.divide(p, self.point_list[i + 1])

        return Curve(nps)

    def subdivided(self):
        self.point_list = self.sub().point_list

    def subdivided_n_times(self, n):
        new_curve = Curve(self.origin_point_list)
        for i in range(n):
            new_curve = new_curve.sub()
        self.point_list = new_curve.point_list


    def hit(self, x):
        for i, p in enumerate(self.point_list):
            if x < p.x:
                pp = self.point_list[i - 1]
                return pp.y + (x - pp.x) / (p.x - pp.x) * (p.y - pp.y)

    def sample_repr(self, lx=60, ly=20, fixed_y_range=None):
        xrange = self.point_list[-1].x - self.point_list[0].x
        x_step = xrange / lx
        ys = [
            self.hit(x_step * (i + 0.5))
            for i in range(lx)
        ]

        if not fixed_y_range:
            y_range = max(ys) - min(ys)
            y_min = min(ys)
        else:
            y_range = fixed_y_range[1] - fixed_y_range[0]
            y_min = fixed_y_range[0]

        y_step = y_range / ly

        y_grid = [
            (i - y_min) // y_step
            for i in ys
        ]
        print("\n↓ point count: {}\n".format(len(self.point_list)))
        for iy in range(ly):
            tp = ""
            for y in y_grid:
                if ly - iy - 1 == y:
                    tp += "#"
                else:
                    tp += " "
            print(tp)

    def sample_x(self, num: int) -> np.ndarray:
        return np.linspace(self.point_list[0].x, self.point_list[-1].x, num)

    def point_count(self):
        return len(self.point_list)

    def put_curve_to_plt(self, plt_local: matplotlib.pyplot, x_samples:np.ndarray, color):
        y = [
            self.hit(x)
            for x in x_samples
        ]
        plt_local.plot(x_samples, y, color=color)

    def push_curve_to_plt(self, plt_local: matplotlib.pyplot, x_samples:np.ndarray, color):
        y = [
            self.hit(x)
            for x in x_samples
        ]
        self.__plt_history.append([y, color])
        for y, color in self.__plt_history:
            plt_local.plot(x_samples, y, color=color)

    def clear_plt_history(self):
        self.__plt_history = []

    def illustrate(self, subdivide_times=6, show_process=True, show_origin=True):
        local_curve = Curve(self.origin_point_list)
        ps_sample_x = local_curve.sample_x(1000)

        dark_color = np.array([0.06, 0.06, 0.06])
        light_color = np.array([0.8, 0.8, 0.8])

        if show_origin:
            local_curve.put_curve_to_plt(plt, ps_sample_x, light_color)

        for i in range(subdivide_times):
            local_curve.subdivided()
            if show_process:
                local_curve.put_curve_to_plt(plt, ps_sample_x, light_color)

        local_curve.put_curve_to_plt(plt, ps_sample_x, dark_color)
        plt.xlabel('x label')
        plt.ylabel('y label')
        plt.title('Nurbs Curve Subdivision')
        plt.show()

    def illustrate_animation(self, subdivide_times=6, duration:float=1, axvline=None):
        self.clear_plt_history()
        local_curve = Curve(self.origin_point_list)
        ps_sample_x = local_curve.sample_x(1000)

        dark_color = np.array([0.06, 0.06, 0.06])
        light_color = np.array([0.8, 0.8, 0.8])

        plt.xlabel('x label')
        plt.ylabel('y label')
        plt.title('Nurbs Curve Subdivision')

        local_curve.push_curve_to_plt(plt, ps_sample_x, light_color)
        plt.pause(duration)

        for i in range(subdivide_times):
            local_curve.subdivided()
            local_curve.push_curve_to_plt(plt, ps_sample_x, light_color)
            plt.pause(duration)

        local_curve.push_curve_to_plt(plt, ps_sample_x, dark_color)
        if axvline:
            plt.axvline(x=axvline, color='orange', linestyle='--')
        plt.show()
        self.clear_plt_history()



if __name__ == '__main__':

    ps = [(0, 0.5), (0.25, 1), (0.5, 0), (0.75, 0.8), (1, 0.3)]
    # ps = [(0, 1), (0.8, 0.8), (1, 0)]
    # ps = [(0, 1), (0.45, 1), (0.55, 0), (1, 0)]

    ps = [Point(i) for i in ps]
    curve = Curve(ps)
    curve.illustrate_animation(
        subdivide_times=4,
        duration=0.2,
        axvline=0.2
    )

    curve.subdivided_n_times(4)
    print(f"f({0.2}) = {curve.hit(0.2)}")
    print(f"curve point count: {curve.point_count()}")
